﻿#pragma once

#include "common.h"
#include "serializable.h"
#include <set>
#include <map>

// ANIP_TODO: DataStore::cleanUp() releases unused detached entities;
// also CLEAN UP THIS BADLY ARRANGED HEADER FILE

namespace anip
{
	class Texture2D;
	class Sequence;
	class FrameGroup;

	class DataStore
	{
	private:
		class SerializeHelper;

	public:
		class Directory;

		class Entity : public Serializable<Entity>
		{
			friend class DataStore::Directory;
			friend class DataStore::SerializeHelper;

			serializable_abstract;

		protected:
			int _id = -1;
			Directory* _parent = nullptr;

		public:
			wstring name = L"<uninitialized>";
			std::set<wstring> tags;
			int id() const { return _id; }
			Directory* parent() const { return _parent; }

			void moveTo(Directory* dir);
			void detach() { moveTo(nullptr); }
		};

		class ReferenceOwner { };

	private:
		static int _maxId;
		static std::set<int> _usableIds;
		static std::map<int, Entity*> _allEntities;

	public:
		static int newId();

	private:
		template<class T>
		class Object : public Entity
		{
			friend class DataStore::SerializeHelper;
		public:
			class Reference;

		protected:
			T* _pointer = nullptr; // we allow it to be nullptr to facilitate deserialization
			std::set<Reference*> _refs;
			Object(int id, Directory* dir = nullptr, T* pt = nullptr)
				: _pointer(pt) 
			{
				_id = id;
				_parent = dir;
			}

		public:
			Object(const Object&) = delete;

			const std::set<Reference*> references() const { return _refs; }
			int referenceCount() const { return _refs.size(); }
			T* pointer() const { return _pointer; }
		};

	public:
		class TextureObject : public Object<Texture2D>
		{
			friend class DataStore;
			using Object::Object;

			serializable_id(TextureObject, 1);

		public:
			bool isExternal = false;
			wstring path;
		};

		class SequenceObject : public Object<Sequence>
		{
			friend class DataStore;
			using Object::Object;

			serializable_id(SequenceObject, 2);
		};

		class FrameGroupObject : public Object<FrameGroup>
		{
			friend class DataStore;
			using Object::Object;

			serializable_id(FrameGroupObject, 3);
		};

		class Directory : public Entity
		{
			friend class DataStore;
			serializable_id(Directory, 0);

		private:
			std::map<int, Entity*> _entities;
			Directory(int id)
			{
				_id = id;
			}

		public:
			Directory(const Directory&) = delete;

			TextureObject& newTexture(Texture2D* tex, int id = -1);
			SequenceObject& newSequence(Sequence* seq, int id = -1);
			FrameGroupObject& newFrameGroup(FrameGroup* fg, int id = -1);
			Directory& newDirectory(const wstring& name, int id = -1);

			const std::map<int, Entity*> entities() const
			{
				return _entities;
			}

			template<class T>
			T& get(int id)
			{
				return dynamic_cast<T&>(*_entities[id]);
			}

			void remove(int id)
			{
				_entities[id]->_parent = nullptr;
				_entities.erase(id);
			}

			void add(Entity& ent)
			{
				assert(ent._parent == nullptr);
				ent._parent = this;
				_entities.insert({ ent._id, &ent });
			}
		};

		template<class T>
		static T& getOrCreateFromId(int id)
		{
			assert(id > 0);
			auto f = _allEntities.find(id);
			if (f == _allEntities.end())
			{
				auto obj = new T(id);
				_allEntities.insert(std::make_pair(id, obj));
				return *obj;
			}
			return dynamic_cast<T&>(*f->second);
		}
		
		static Directory root;

		static void serialize(std::ostream& out);
		static void deserialize(std::istream& in);
	};

	typedef DataStore::TextureObject STexture2D;
	typedef DataStore::SequenceObject SSequence;
	typedef DataStore::FrameGroupObject SFrameGroup;
	typedef DataStore::Directory SDirectory;
}

template<>
anip::DataStore::Directory& anip::DataStore::getOrCreateFromId(int id);

template<class T>
class anip::DataStore::Object<T>::Reference
{
private:
	Object<T>* _obj = nullptr;
	ReferenceOwner& _owner;

public:
	Reference(ReferenceOwner& owner) : _owner(owner) {}
	Reference(const Reference&) = delete;

	Object<T>* object() const
	{
		return _obj;
	}

	ReferenceOwner& owner() const
	{
		return _owner;
	}

	operator bool() const { return _obj != nullptr; }
	operator T& () { return *_obj->pointer(); }
	operator const T& () const { return *_obj->pointer(); }

	void assign(Object<T>* nobj)
	{
		if (nobj != nullptr)
			nobj->_refs.insert(this);
		if (_obj != nullptr)
			_obj->_refs.erase(this);
		_obj = nobj;
	}
};